package com.nway.pdf.signatures;

import java.awt.AlphaComposite;
import java.awt.Font;
import java.awt.Graphics2D;
import java.awt.Transparency;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.net.MalformedURLException;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.security.Security;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;

import javax.imageio.ImageIO;
import javax.security.auth.x500.X500PrivateCredential;

import org.bouncycastle.asn1.x500.X500NameBuilder;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;

import com.itextpdf.text.DocumentException;
import com.itextpdf.text.Image;
import com.itextpdf.text.pdf.AcroFields;
import com.itextpdf.text.pdf.PdfContentByte;
import com.itextpdf.text.pdf.PdfReader;
import com.itextpdf.text.pdf.PdfSignatureAppearance;
import com.itextpdf.text.pdf.PdfStamper;
import com.itextpdf.text.pdf.PdfStream;
import com.itextpdf.text.pdf.parser.ImageRenderInfo;
import com.itextpdf.text.pdf.parser.PdfImageObject;
import com.itextpdf.text.pdf.parser.PdfReaderContentParser;
import com.itextpdf.text.pdf.parser.RenderListener;
import com.itextpdf.text.pdf.parser.TextRenderInfo;
import com.itextpdf.text.pdf.security.BouncyCastleDigest;
import com.itextpdf.text.pdf.security.DigestAlgorithms;
import com.itextpdf.text.pdf.security.ExternalDigest;
import com.itextpdf.text.pdf.security.ExternalSignature;
import com.itextpdf.text.pdf.security.MakeSignature;
import com.itextpdf.text.pdf.security.MakeSignature.CryptoStandard;
import com.itextpdf.text.pdf.security.PdfPKCS7;
import com.itextpdf.text.pdf.security.PrivateKeySignature;

/**
 * 标记PDF文件，防修改
 * <p>
 * 方式：签名、嵌入加密信息
 * <p>
 * 每人每年一个证书,证书有效期一年
 * <p>
 * 确定签名人信息：
 * <p>
 * 1、若pdf证书存在且有效，可通过证书信息确认<br>
 * 2、若pdf证书不存在或无效<br>
 *   1)、调用 {@link #obtainImage(String)} ,取得文件中加密图片<br>
 *   2)、解密图片内容, 解密后信息包含时间<br>
 *   3)、核查图片位置，标准图片位置：{x: {秒 / 3.0f} ,y: {分钟 * 19 / 3.0f} },文件中图片位置在图片文件同名的{imgSeq}-StartPoint.txt文件中
 * 
 * @author zdtjss@163.com
 * @since 2015-06-28
 *
 */
public class SignPdf {

	private static final char[] KEYSTORE_PASSWORD = "abc123".toCharArray();
	
	private static final int IMG_X = 386;
	private static final int IMG_Y = 10;
	
	private static final int FIRST_PAGE_INDEX = 1;
	
	public static void main(String[] args) throws Exception {

        SignPdf app = new SignPdf();
        SignatureUser user = new SignatureUser();
        
        user.setId("123");
        user.setName("XX");
        user.setOrgUnitName("XX办公室");
        user.setOrganization("XX部门");
        user.setLocalityName("XX市");
        user.setProvinceName("XX省");
        user.setSigningLocation("192.168.1.1");
        
		app.sign("word2pdf.pdf", "word2pdf_mod.pdf", user);
		
        app.obtainImage("word2pdf_mod.pdf","000");
        
//		app.createKeyStore(user, new File(KEYSTORE), KEYSTORE_PASSWORD);
        
//        app.verifySignatures("word2pdf_mod.pdf");
        
	}

	/**
	 * PDF签名，并向PDF中写入唯一信息
	 * 
	 * @param srcPdf
	 *            需要签名标记的PDF
	 * @param destPdf
	 *            签名标记后的PDF
	 * @param user
	 *            签名用户信息
	 * 
	 * @throws IOException
	 * @throws DocumentException
	 * @throws GeneralSecurityException
	 */
	public void sign(String srcPdf, String destPdf, SignatureUser user)
			throws IOException, DocumentException, GeneralSecurityException {

		PdfReader reader = new PdfReader(srcPdf);
		
		FileOutputStream fis = new FileOutputStream(destPdf);
		
		PdfStamper stamper = PdfStamper.createSignature(reader, fis, '\0');

		//加入标识
		addImage(stamper, user.getId());
		
		//签名
		signature(stamper, user);
		
		stamper.close();
		
		fis.close();
	}
	
	/**
	 * 图片位置 X:0 Y: { currentMinute * 10 }
	 * 
	 * @param stamper
	 * @param uid 图片内容
	 * @throws MalformedURLException
	 * @throws IOException
	 * @throws DocumentException
	 */
	private void addImage(PdfStamper stamper, String uid) throws MalformedURLException,
			IOException, DocumentException {

		// 第一页
		PdfContentByte under = stamper.getUnderContent(FIRST_PAGE_INDEX);
        
        Date createTime = new Date();
		
		Image img = Image.getInstance(createImage(uid， createTime));
		
		// 背景
		img.setAlignment(Image.UNDERLYING);
		// 图片大小
		img.scaleAbsolute(IMG_X, IMG_Y);
		// 格式
		img.setOriginalType(Image.ORIGINAL_PNG);
		// 不压缩
		img.setCompressionLevel(PdfStream.NO_COMPRESSION);
		// 图片位置
		img.setAbsolutePosition(0, Calendar.getInstance().get(Calendar.MINUTE) * 10);

		under.addImage(img);
		
		Image img1 = Image.getInstance(createImage(uid));
		
		// 背景
		img1.setAlignment(Image.UNDERLYING);
		// 图片大小
		img1.scaleAbsolute(IMG_X, IMG_Y);
		// 格式
		img1.setOriginalType(Image.ORIGINAL_PNG);
		// 不压缩o
		img1.setCompressionLevel(PdfStream.NO_COMPRESSION);
        
		Calendar cal = Calendar.getInstance();

    	cal.setTime(createTime);

		float x = cal.get(Calendar.SECOND) / 3.0f;
		float y = cal.get(Calendar.MINUTE) * 19 / 3.0f;

		// 图片位置 属附加加密信息 请勿更改
		img.setAbsolutePosition(x, y);
		
		under.addImage(img1);
	}
	
	private void signature(PdfStamper stamper, SignatureUser user)
			throws FileNotFoundException, IOException, DocumentException,
			GeneralSecurityException {

		PdfSignatureAppearance appearance = stamper.getSignatureAppearance();
		
		//不允许修改
		appearance.setCertificationLevel(PdfSignatureAppearance.CERTIFIED_NO_CHANGES_ALLOWED);
		
		appearance.setReason(user.getOrgUnitName() + user.getName() + " 获取");
		appearance.setLocation(user.getSigningLocation());

		ExternalDigest digest = new BouncyCastleDigest();
		
		BouncyCastleProvider provider = new BouncyCastleProvider();
		
		Security.addProvider(provider);
		
        StringBuilder keyStoreFilePath = new StringBuilder();
    	
		keyStoreFilePath.append(createResourcePath());
		keyStoreFilePath.append(File.separator);
		keyStoreFilePath.append("keyStore");
		keyStoreFilePath.append(File.separator);
		keyStoreFilePath.append(user.getName());
		keyStoreFilePath.append('-');
		keyStoreFilePath.append(user.getId());
		keyStoreFilePath.append('-');
		// 每人每年一个
		keyStoreFilePath.append(Calendar.getInstance().get(Calendar.YEAR));
		keyStoreFilePath.append(".keystore");
		
		// {webapp}/apps/signPdf/images
		File keyStoreFile = new File(keyStoreFilePath.toString());
		
		KeyStore ks = null;
		
		if (keyStoreFile.exists()) {
			ks = KeyStore.getInstance(KeyStore.getDefaultType());
			ks.load(new FileInputStream(keyStoreFile), KEYSTORE_PASSWORD);
		} else {
			ks = createKeyStore(user, keyStoreFile, KEYSTORE_PASSWORD);
		}
		
		String alias = (String) ks.aliases().nextElement();
        
        PrivateKey pk = (PrivateKey) ks.getKey(alias, KEYSTORE_PASSWORD);
        
        Certificate[] chain = ks.getCertificateChain(alias);
        
		ExternalSignature signature = new PrivateKeySignature(pk, DigestAlgorithms.SHA512, provider.getName());
		
		MakeSignature.signDetached(appearance, digest, signature, chain, null, null, null, 0, CryptoStandard.CMS);
	}
	
	/**
	 * 签名校验
	 * 
	 * @param path
	 * @throws IOException
	 * @throws GeneralSecurityException
	 */
	public void verifySignatures(String path,String keyStore) throws IOException, GeneralSecurityException {
		
        PdfReader reader = new PdfReader(path);
        AcroFields fields = reader.getAcroFields();
        ArrayList<String> names = fields.getSignatureNames();
        
        BouncyCastleProvider provider = new BouncyCastleProvider();
		KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
		
		Security.addProvider(provider);
		
		ks.load(new FileInputStream(keyStore), KEYSTORE_PASSWORD);

		String alias = (String) ks.aliases().nextElement();
        
        Certificate[] chain = ks.getCertificateChain(alias);
		
		for (String name : names) {
			
	        PdfPKCS7 pkcs7 = fields.verifySignature(name);
	       
	        if(!pkcs7.verify()) {
	        	
	        	System.out.println("文档已被修改");
	        	break;
	        }
	        
			// 证书校验 创建证书时没有证书链
			pkcs7.getCertificates()[0].verify(chain[0].getPublicKey());
		}
		
		System.out.println("文档未被修改");
	}
	
	/**
	 * 创建标记图片
	 * 
	 * @param uid
	 *            图片内容
	 * @return
	 * @throws IOException
	 */
	private byte[] createImage(String uid, Date createTime) throws IOException {
		
		// 创建BufferedImage对象
		BufferedImage image = new BufferedImage(IMG_X, IMG_Y, BufferedImage.TYPE_INT_RGB);
		// 获取Graphics2D
		Graphics2D g2d = image.createGraphics();
		
		// ----------  增加下面的代码使得背景透明  -----------------
		image = g2d.getDeviceConfiguration().createCompatibleImage(IMG_X, IMG_Y, Transparency.TRANSLUCENT);
		g2d.dispose();
		g2d = image.createGraphics();
		// ----------  背景透明代码结束  -----------------
		
		g2d.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 11));
		
		// 设置透明度 1.0f为透明度 ，值从0-1.0，依次变得不透明
		g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC, 0.001f));
		
		String id = uid + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
		
		// 加密方式 非必要 不可修改 修改须记录
		g2d.drawString(EncryptionUtil.encryptAES(id, uid + " "), 0, 10);
		
		g2d.dispose();
		
        ByteArrayOutputStream byteImage = new ByteArrayOutputStream();

		// 保存文件    
		if(ImageIO.write(image, "png", byteImage)) {
			
			return byteImage.toByteArray();
		}
		
		return null;
	}
	
	/**
	 * 图片创建时使用EncryptionUtil.encryptDES加密，获取图片内容后需使用EncryptionUtil.decryptDES解密
	 * <p>
	 * <b>须保证签名正确有效</b>
	 * 
	 * @param pdf
	 *            PDF路径
	 * @param imgName
	 *            图片路径  基础名称
	 * @throws IOException
	 */
	public void obtainImage(String pdf, final String imgName) throws IOException {

		PdfReader pdfReader = new PdfReader(pdf);
		
		PdfReaderContentParser parser = new PdfReaderContentParser(pdfReader);
		
		parser.processContent(FIRST_PAGE_INDEX, new RenderListener() {

			int imgNameSeq = 0;
    		String imgPath = createResourcePath() + File.separator +"images" + File.separator ;
			
			public void renderImage(ImageRenderInfo renderInfo) {
				
				++imgNameSeq;
				
				try {
					
					String ext = renderInfo.getImage().getImageBytesType().getFileExtension();
					
					// 写图片
					FileOutputStream fos = new FileOutputStream(imgPath + imgNameSeq + "." + ext);

					fos.write(renderInfo.getImage().getImageAsBytes());
					
					fos.close();
					
					// 写图片坐标
					FileWriter fw = new FileWriter(imgPath + imgNameSeq + "-StartPoint.txt");
					BufferedWriter bw = new BufferedWriter(fw);

					Vector point = renderInfo.getStartPoint();

					bw.write("x : " + point.get(Vector.I1));
					bw.newLine();
					bw.write("y : " + point.get(Vector.I2));
					
					bw.close();
					fw.close();

				} catch (IOException e) {
					e.printStackTrace();
				}
			}
			
			public void renderText(TextRenderInfo renderInfo) {
			}

			public void beginTextBlock() {
			}

			public void endTextBlock() {
			}
		});
        
        pdfReader.close();
	}
	
	/**
	 * 比较图片
	 * 
	 * @throws IOException
	 */
	private boolean compareImage() throws IOException {
		
		//原图
		BufferedImage img = ImageIO.read(new File("results/chapter2/test.png"));
		//PDF导出图
		BufferedImage img2 = ImageIO.read(new File("results/chapter2/test000.png"));
		
		for (int x = 0; x < img.getWidth(); x++) {
			for (int y = 0; y < img.getHeight(); y++) {
				//透明图片导入PDF 导出后颜色发生了变化  原图同像素RGB减16777216与导出图片相等
				if ((img.getRGB(x, y) - 16777216) != img2.getRGB(x, y)) {
					return false;
				}
			}
		}
		
		return true;
	}
	
	private KeyStore createKeyStore(SignatureUser user, File keystore, char[] keystorePassword) {
		
		try {
			
			BouncyCastleProvider provider = new BouncyCastleProvider();
			
			Security.addProvider(provider);
			
			KeyStore store = KeyStore.getInstance(KeyStore.getDefaultType());
			
			store.load(null, null);
			
			X500PrivateCredential x509 = generateCertificate(user, keystorePassword);
			
			store.setKeyEntry(x509.getAlias(), x509.getPrivateKey(),
					keystorePassword,
					new Certificate[] { x509.getCertificate() });
			
			FileOutputStream fos = new FileOutputStream(keystore);
			
			store.store(fos, keystorePassword);
			
			fos.close();
			
			return store;
			
		} catch (Throwable e) {
			
			e.printStackTrace();
			
			throw new RuntimeException(
					"Failed to generate self-signed certificate!", e);
		}
	}
	
	/**
	 * X.509 v3 
	 * 
	 * @param hostname
	 * @param keystorePassword
	 * @return
	 */
	private X500PrivateCredential generateCertificate(SignatureUser user, char[] keystorePassword) {
		
		try {
			
			BouncyCastleProvider provider = new BouncyCastleProvider();
			
			Security.addProvider(provider);
			
			KeyPairGenerator kpGen = KeyPairGenerator.getInstance("RSA", provider);
			
			kpGen.initialize(1024, new SecureRandom());
			
			KeyPair pair = kpGen.generateKeyPair();
			
			// 使用者
			X500NameBuilder issuer = new X500NameBuilder(BCStyle.INSTANCE);
			
			issuer.addRDN(BCStyle.CN, user.getName());
			issuer.addRDN(BCStyle.OU, user.getOrgUnitName());
			issuer.addRDN(BCStyle.O, user.getOrganization());
			
			issuer.addRDN(BCStyle.L, user.getLocalityName());
			issuer.addRDN(BCStyle.ST, user.getProvinceName());
			issuer.addRDN(BCStyle.C, Locale.CHINA.getCountry());
			
			// 颁发者
			X500NameBuilder subject = new X500NameBuilder(BCStyle.INSTANCE);
			
			subject.addRDN(BCStyle.CN, "XX产品研发组");
			subject.addRDN(BCStyle.OU, "XX事业部");
			subject.addRDN(BCStyle.O, "XX有限公司");
			
			subject.addRDN(BCStyle.L, "XX市");
			subject.addRDN(BCStyle.ST, "XX省");
			subject.addRDN(BCStyle.C, Locale.CHINA.getCountry());
			
			// 证书有效期 begin
			Calendar calendar = Calendar.getInstance();
			
			Date notBefore = calendar.getTime();
			
			calendar.set(Calendar.YEAR, calendar.get(Calendar.YEAR) + 1);
			
			Date notAfter = calendar.getTime();
			// 证书有效期 end
			
			BigInteger serial = BigInteger.valueOf(System.currentTimeMillis());
			
			X509v3CertificateBuilder certGen = new JcaX509v3CertificateBuilder(
					subject.build(), serial, notBefore, notAfter,
					issuer.build(), pair.getPublic());
			
			ContentSigner sigGen = new JcaContentSignerBuilder(
					"SHA256WithRSAEncryption").setProvider(provider).build(
							pair.getPrivate());
			
			X509Certificate cert = new JcaX509CertificateConverter()
			.setProvider(provider)
			.getCertificate(certGen.build(sigGen));
			
			return new X500PrivateCredential(cert, pair.getPrivate(), user.getOrgUnitName() + user.getName());
			
		} catch (Throwable e) {
			
			e.printStackTrace();
			
			throw new RuntimeException(
					"Failed to generate self-signed certificate!", e);
		}
	}
    
    private String createResourcePath() {
    	
		File signPdfClassFile = new File(SignPdf.class.getClassLoader().getResource("com/nway/pdf/signatures/SignPdf.class").getPath());
		
		return signPdfClassFile.getParentFile().getParentFile().getParentFile().getParentFile().getParentFile().getParent();
	}
	
}